game_manager_lib\commands\recommendation/
analysis.rs1use crate::database::AppState;
7use crate::errors::AppError;
8use crate::services::recommendation::{
9 calculate_user_profile, export_games_csv, export_report_json, export_report_txt,
10 generate_analysis_report, parse_release_year, GameWithDetails, RecommendationConfig,
11 UserSettings,
12};
13use serde::Serialize;
14use std::collections::HashSet;
15use tauri::{AppHandle, Manager, State};
16
17#[derive(Debug, Serialize)]
19pub struct AnalysisResponse {
20 pub success: bool,
21 pub json_path: Option<String>,
22 pub csv_path: Option<String>,
23 pub txt_path: Option<String>,
24 pub message: String,
25}
26
27#[tauri::command]
36pub async fn generate_recommendation_analysis(
37 app: AppHandle,
38 limit: Option<usize>,
39) -> Result<AnalysisResponse, String> {
40 tracing::info!("Gerando análise de recomendação...");
41
42 let analysis_dir = setup_analysis_directory(&app)?;
43 let (json_path, txt_path, csv_path) = create_analysis_file_paths(&analysis_dir)?;
44
45 let state: State<AppState> = app.state();
46 let (candidates_with_details, all_games_with_details, already_played_ids) =
47 fetch_and_prepare_data(&state)?;
48
49 let profile = calculate_user_profile(&all_games_with_details, &HashSet::new());
50 let (cf_scores, _) =
51 crate::services::cf_aggregator::build_cf_candidates(&all_games_with_details);
52
53 let config = RecommendationConfig::default();
54 let user_settings = UserSettings::default();
55
56 let report = generate_analysis_report(
57 &profile,
58 &candidates_with_details,
59 &cf_scores,
60 &already_played_ids,
61 config,
62 user_settings,
63 );
64
65 let limited_report = limit_report(report, limit);
66
67 export_analysis_reports(&limited_report, &json_path, &txt_path, &csv_path)?;
68
69 log_success(&json_path, &txt_path, &csv_path);
70
71 Ok(AnalysisResponse {
72 success: true,
73 json_path: Some(json_path.to_string_lossy().to_string()),
74 txt_path: Some(txt_path.to_string_lossy().to_string()),
75 csv_path: Some(csv_path.to_string_lossy().to_string()),
76 message: format!(
77 "Análise gerada com sucesso! {} jogos analisados.",
78 limited_report.games.len()
79 ),
80 })
81}
82
83fn setup_analysis_directory(app: &AppHandle) -> Result<std::path::PathBuf, String> {
86 let analysis_dir = app
87 .path()
88 .app_data_dir()
89 .map_err(|e| format!("Erro ao obter diretório de dados: {}", e))?
90 .join("analysis");
91
92 std::fs::create_dir_all(&analysis_dir)
93 .map_err(|e| format!("Erro ao criar diretório de análise: {}", e))?;
94
95 Ok(analysis_dir)
96}
97
98fn create_analysis_file_paths(
99 analysis_dir: &std::path::Path,
100) -> Result<(std::path::PathBuf, std::path::PathBuf, std::path::PathBuf), String> {
101 let timestamp = chrono::Local::now().format("%Y%m%d_%H%M%S");
102 let json_path = analysis_dir.join(format!("recommendation_analysis_{}.json", timestamp));
103 let txt_path = analysis_dir.join(format!("recommendation_analysis_{}.txt", timestamp));
104 let csv_path = analysis_dir.join(format!("recommendation_ranking_{}.csv", timestamp));
105
106 Ok((json_path, txt_path, csv_path))
107}
108
109fn fetch_and_prepare_data(
110 state: &State<AppState>,
111) -> Result<(Vec<GameWithDetails>, Vec<GameWithDetails>, HashSet<String>), String> {
112 let library_games = crate::commands::games::get_games(state.clone())
113 .map_err(|e| format!("Erro ao buscar jogos da biblioteca: {}", e))?;
114
115 tracing::info!("Total de jogos na biblioteca: {}", library_games.len());
116
117 let already_played_ids: HashSet<String> = library_games
118 .iter()
119 .filter(|g| {
120 let hours = g.playtime.unwrap_or(0) as f32 / 60.0;
121 hours > 5.0 || g.favorite
122 })
123 .map(|g| g.id.clone())
124 .collect();
125
126 let candidate_games: Vec<_> = library_games
127 .iter()
128 .filter(|g| !already_played_ids.contains(&g.id))
129 .cloned()
130 .collect();
131
132 tracing::info!("Candidatos para recomendação: {}", candidate_games.len());
133
134 let candidates_with_details = fetch_games_with_details(&candidate_games, state)
135 .map_err(|e| format!("Erro ao processar candidatos: {}", e))?;
136
137 let all_games_with_details = fetch_games_with_details(&library_games, state)
138 .map_err(|e| format!("Erro ao processar biblioteca completa: {}", e))?;
139
140 Ok((
141 candidates_with_details,
142 all_games_with_details,
143 already_played_ids,
144 ))
145}
146
147fn fetch_games_with_details(
148 _games: &[crate::models::Game],
149 state: &State<AppState>,
150) -> Result<Vec<GameWithDetails>, AppError> {
151 let conn = state.library_db.lock()?;
152
153 let mut stmt = conn.prepare(
154 "SELECT
155 g.id, g.name, g.playtime, g.favorite, g.user_rating, g.cover_url,
156 g.platform_id, g.last_played, g.added_at, g.platform,
157 gd.genres, gd.steam_app_id, gd.release_date, gd.series, gd.tags
158 FROM games g
159 LEFT JOIN game_details gd ON g.id = gd.game_id
160 ORDER BY g.name ASC",
161 )?;
162
163 let games_with_details: Result<Vec<GameWithDetails>, _> = stmt
164 .query_map([], |row| {
165 let game = crate::models::Game {
166 id: row.get(0)?,
167 name: row.get(1)?,
168 playtime: row.get(2)?,
169 favorite: row.get(3)?,
170 user_rating: row.get(4)?,
171 cover_url: row.get(5)?,
172 platform_id: row.get(6)?,
173 last_played: row.get(7)?,
174 added_at: row.get(8)?,
175 platform: row
176 .get::<_, String>(9)
177 .unwrap_or_else(|_| "Unknown".to_string()),
178 genres: None,
180 developer: None,
181 install_path: None,
182 executable_path: None,
183 launch_args: None,
184 status: None,
185 is_adult: false,
186 };
187
188 let genres_json: Option<String> = row.get(10)?;
189 let genres: Vec<String> = genres_json
190 .as_ref()
191 .map(|s| {
192 if let Ok(vec) = serde_json::from_str::<Vec<String>>(s) {
194 vec
195 } else {
196 s.split(',')
198 .map(|g| g.trim().to_string())
199 .filter(|g| !g.is_empty())
200 .collect()
201 }
202 })
203 .unwrap_or_default();
204
205 let steam_app_id_str: Option<String> = row.get(11)?;
206 let steam_app_id: Option<u32> = steam_app_id_str.and_then(|s| s.parse().ok());
207
208 let release_date: Option<String> = row.get(12)?;
209 let release_year = release_date.and_then(|d| parse_release_year(&d));
210 let series: Option<String> = row.get(13)?;
211
212 let tags_json: Option<String> = row.get(14)?;
214 let tags: Vec<crate::models::GameTag> = tags_json
215 .as_ref()
216 .and_then(|s| serde_json::from_str(s).ok())
217 .unwrap_or_default();
218
219 Ok(GameWithDetails {
220 game,
221 genres,
222 tags,
223 series,
224 release_year,
225 steam_app_id,
226 })
227 })?
228 .collect();
229
230 games_with_details.map_err(|e| e.into())
231}
232
233fn limit_report(
234 mut report: crate::services::recommendation::RecommendationAnalysisReport,
235 limit: Option<usize>,
236) -> crate::services::recommendation::RecommendationAnalysisReport {
237 if let Some(limit) = limit {
238 report.games.truncate(limit);
239 }
240 report
241}
242
243fn export_analysis_reports(
244 report: &crate::services::recommendation::RecommendationAnalysisReport,
245 json_path: &std::path::Path,
246 txt_path: &std::path::Path,
247 csv_path: &std::path::Path,
248) -> Result<(), String> {
249 export_report_json(report, json_path.to_str().unwrap())
250 .map_err(|e| format!("Erro ao salvar JSON: {}", e))?;
251
252 export_report_txt(report, txt_path.to_str().unwrap())
253 .map_err(|e| format!("Erro ao salvar TXT: {}", e))?;
254
255 export_games_csv(&report.games, csv_path.to_str().unwrap())
256 .map_err(|e| format!("Erro ao salvar CSV: {}", e))?;
257
258 Ok(())
259}
260
261fn log_success(
262 json_path: &std::path::Path,
263 txt_path: &std::path::Path,
264 csv_path: &std::path::Path,
265) {
266 tracing::info!("Análise gerada com sucesso!");
267 tracing::info!(" JSON: {:?}", json_path);
268 tracing::info!(" TXT: {:?}", txt_path);
269 tracing::info!(" CSV: {:?}", csv_path);
270}